package org.metasketch.reflection;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

public class RefUtil {

	Map<Class, Map<String, Method>> getters = new ConcurrentHashMap<Class, Map<String, Method>>();

	public boolean isPrimitiveOrProxy(Class type) {
		return type.isPrimitive() || type == Integer.class
				|| type == Long.class || type == Double.class
				|| type == Short.class || type == Character.class
				|| type == Float.class || type == Double.class
				|| type == Byte.class || type == Date.class
				|| type == BigDecimal.class || type == BigInteger.class;
	}

	public boolean isPrimitiveOrProxyOrString(Class type) {
		return type.isPrimitive() || type == Integer.class
				|| type == Long.class || type == Double.class
				|| type == Short.class || type == Character.class
				|| type == Float.class || type == Double.class
				|| type == Byte.class || type == Date.class
				|| type == BigDecimal.class || type == BigInteger.class
				|| type == String.class;

	}

	public Class isSpecificList(Field field) {
		Type type = field.getGenericType();
		if (type instanceof ParameterizedType) {
			ParameterizedType ptype = (ParameterizedType) field
					.getGenericType();
			Type[] targs = ptype.getActualTypeArguments();
			if (targs == null || targs.length == 0)
				return null;
			for (int j = 0; j < targs.length; j++) {
				Class tclas = (Class) targs[j];
				return tclas;
			}
		} else
			return null;
		return null;
	}

	public Class isSpecificList(Class type, String fieldName) {
		Field field = getField(type, fieldName);
		if (field == null)
			return null;

		return isSpecificList(field);
	}

	public Field getField(Class type, String fieldName) {
		try {
			Field field = type.getDeclaredField(fieldName);
			return field;
		} catch (Throwable e) {

			Class parent = type.getSuperclass();
			if (parent == null)
				return null;

			return getField(parent, fieldName);

		}
	}

	public Object getValue(Object obj, String fieldName) throws Exception {

		int index = fieldName.indexOf('.');
		if (index < 0) {

			Class listType = isSpecificList(obj.getClass(), fieldName);
			if (listType == null) {
				return get(obj, fieldName);
			} else {
				Collection list = (Collection) get(obj, fieldName);

				return list;

				// if (list.size() == 0)
				// return null;
				//
				// Object value = list.get(0);
				// return value;
			}
		} else {
			String currentField = fieldName.substring(0, index);
			Object field = get(obj, currentField);

			if (field == null)
				return null;

			String nextField = fieldName.substring(index + 1);

			return getValue(field, nextField);
		}

	}

	public void setValue(Object obj, String fieldName, Object value)
			throws Exception {

		int index = fieldName.indexOf('.');
		if (index < 0) {

			// get(obj, fieldName);

			Class listType = isSpecificList(obj.getClass(), fieldName);
			if (listType == null) {
				setFieldValue(obj, value, fieldName);
			} else {
				Object collection = get(obj, fieldName);
				if (collection instanceof List)
					((List) collection).add(value);
				else if (collection instanceof Set)
					((Set) collection).add(value);
				// return value;
			}

		} else {
			String currentField = fieldName.substring(0, index);
			Object field = get(obj, currentField);

			if (field == null)
				throw new NullPointerException("Null value at field "
						+ fieldName + " on Parent object of type "
						+ obj.getClass().getCanonicalName());

			String nextField = fieldName.substring(index + 1);

			setValue(field, nextField, value);
		}

	}

	private Object get(Object obj, String fieldName) throws Exception {
		Method getter = null;
		Map<String, Method> map = getters.get(obj.getClass());
		if (map != null) {
			getter = map.get(fieldName);

			if (getter != null)
				return getter.invoke(obj, null);

			getter = getGetter(obj.getClass(), fieldName);
			map.put(fieldName, getter);

			return getter.invoke(obj, null);

		}

		map = new ConcurrentHashMap<String, Method>();
		getters.put(obj.getClass(), map);
		getter = getGetter(obj.getClass(), fieldName);
		map.put(fieldName, getter);

		return getter.invoke(obj, null);

	}

	public Method getGetter(Class type, String fieldName) throws Exception {
		String getterName = "get" + fieldName.substring(0, 1).toUpperCase()
				+ fieldName.substring(1);
		try {
			return type.getMethod(getterName, null);
		} catch (Throwable t) {
			// silent catch
		}

		getterName = "is" + fieldName.substring(0, 1).toUpperCase()
				+ fieldName.substring(1);

		return type.getMethod(getterName, null);
	}

	// TODO improve this A LOT to allow for obj.chil.chil.field
	// public void setValue(Object obj, String fieldName, Object value)
	// throws Exception {
	//
	// setFieldValue(obj, value, fieldName);
	//
	// }

	// TODO improve this A LOT to allow for obj.chil.chil.field
	public void setNullValue(Object obj, String fieldName, Class childType)
			throws Throwable {

		Object[] args = new Object[1];
		args[0] = null;

		Method setter = null;
		try {
			setter = getSetter(obj.getClass(), childType, fieldName);
		} catch (Throwable t) {
			if (childType.equals(Boolean.class)) {
				setter = getSetter(obj.getClass(), boolean.class, fieldName);
			}
		}

		setter.invoke(obj, args);

	}

	public void setNullValue(Object object, String fieldName) throws Exception {
		Field field = getField(object.getClass(), fieldName);
		Class<?> childType = field.getType();
		try {
			setNullValue(object, fieldName, childType);
		} catch (Throwable e) {
			throw new Exception(e);
		}

	}

	public Method getSetter(Class type, Class<?> paramType, String fieldName)
			throws Exception {

		String fn = "set" + getFieldNameForGetterAndSetter(fieldName);
		Method[] methods = type.getMethods();
		for (Method method : methods) {
			if (method.getName().equals(fn)) {
				Class[] params = method.getParameterTypes();
				if (params.length != 1)
					continue;

				for (Class pType : params) {
					if (pType.isAssignableFrom(paramType))
						return method;

					if (isWrapperType(paramType)
							&& isWrapperTypeFor(paramType, pType)) {
						return method;
					}
				}

			}

		}

		Method setter = null;
		Class[] params = new Class[1];
		params[0] = paramType;
		setter = type.getMethod("set" + fn, params);
		return setter;

	}

	public static boolean isWrapperType(Class<?> clazz) {
		return clazz.equals(Boolean.class) || clazz.equals(Integer.class)
				|| clazz.equals(Character.class) || clazz.equals(Byte.class)
				|| clazz.equals(Short.class) || clazz.equals(Double.class)
				|| clazz.equals(Long.class) || clazz.equals(Float.class);
	}

	public static boolean isWrapperTypeFor(Class<?> wrapperCandidate,
			Class primitiveType) {
		if (wrapperCandidate.equals(Boolean.class)
				&& primitiveType.equals(boolean.class))
			return true;

		if (wrapperCandidate.equals(Integer.class)
				&& primitiveType.equals(int.class))
			return true;

		if (wrapperCandidate.equals(Long.class)
				&& primitiveType.equals(long.class))
			return true;

		if (wrapperCandidate.equals(Short.class)
				&& primitiveType.equals(short.class))
			return true;

		if (wrapperCandidate.equals(Float.class)
				&& primitiveType.equals(float.class))
			return true;

		if (wrapperCandidate.equals(Double.class)
				&& primitiveType.equals(double.class))
			return true;

		if (wrapperCandidate.equals(Character.class)
				&& primitiveType.equals(char.class))
			return true;

		if (wrapperCandidate.equals(Byte.class)
				&& primitiveType.equals(byte.class))
			return true;

		return false;
	}

	protected String getFieldNameForGetterAndSetter(String fieldName) {
		String fn = fieldName.substring(0, 1).toUpperCase()
				+ fieldName.substring(1);
		return fn;
	}

	protected void setFieldValue(Object parent, Object child, Field field)
			throws Throwable {
		String fieldName = field.getName().toUpperCase();
		fieldName = fieldName.substring(0, 1) + field.getName().substring(1);
		setFieldValue(parent, child, fieldName);

	}

	protected void setFieldValue(Object parent, Object child, String fieldName)
			throws Exception {
		setFieldValue(parent.getClass(), parent, child, fieldName);
	}

	protected Object transform(Object value, Class type) {

		if (isPrimitiveOrProxyOrString(type)) {

			if (value instanceof String) {

				if (type == String.class) {
					return value;
				}

				if (type == boolean.class) {
					return Boolean.valueOf((String) value).booleanValue();
				}

				if (type == Boolean.class) {
					return Boolean.valueOf((String) value);
				}

				if (type == short.class) {
					return Short.valueOf((String) value).shortValue();
				}

				if (type == Short.class) {
					return Short.valueOf((String) value);
				}

				if (type == int.class) {
					return Integer.valueOf((String) value).intValue();
				}

				if (type == Integer.class) {
					return Integer.valueOf((String) value);
				}

				if (type == long.class) {
					return Long.valueOf((String) value).longValue();
				}

				if (type == Long.class) {
					return Long.valueOf((String) value);
				}

				if (type == float.class) {
					return Float.valueOf((String) value).floatValue();
				}

				if (type == Float.class) {
					return Float.valueOf((String) value);
				}

				if (type == double.class) {
					return Double.valueOf((String) value).doubleValue();
				}

				if (type == Double.class) {
					return Double.valueOf((String) value);
				}

				if (type == byte.class) {
					return Byte.valueOf((String) value).byteValue();
				}

				if (type == Byte.class) {
					return Byte.valueOf((String) value);
				}

				if (type == char.class) {
					if (value != null && ((String) value).length() > 0)
						return Character.valueOf(((String) value).charAt(0))
								.charValue();
					else
						return 0;
				}

				if (type == Character.class) {
					if (value != null && ((String) value).length() > 0)
						return Character.valueOf(((String) value).charAt(0));
					else
						return null;
				}

				if (type == BigDecimal.class) {
					return new BigDecimal((String) value);
				}

				if (type == BigInteger.class) {
					return new BigInteger((String) value);
				}

				if (type == Date.class) {
					return new Date((String) value);
				}

				return value;

			} else
				return value;

			// return type.isPrimitive() || type == Integer.class
			// || type == Long.class || type == Double.class
			// || type == Short.class || type == Character.class
			// || type == Float.class || type == Double.class
			// || type == Byte.class || type == Date.class
			// || type == BigDecimal.class || type == BigInteger.class
			// || type == String.class;

		} else
			return value;

		// return value;
	}

	protected void setFieldValue(Class parentType, Object parent, Object child,
			String fieldName) throws Exception {

		Method getter = getGetter(parentType, fieldName);
		Class valueType = getter.getReturnType();

		Object value = transform(child, valueType);

		Object[] args = new Object[1];
		args[0] = value;

		Method setter = null;
		try {
			Class fieldType = value.getClass();
			setter = getSetter(parentType, fieldType, fieldName);

		} catch (Throwable t) {
			if (child.getClass().equals(Boolean.class)) {
				setter = getSetter(parentType, boolean.class, fieldName);
			}
		}

		setter.invoke(parent, args);
	}

}
